/*
 * Copyright (c) 2018-2021, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2022, networkException <networkexception@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Debug.h>
#include <AK/StringBuilder.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/Bindings/HTMLScriptElementPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/HTMLScriptElement.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/Fetching.h>
#include <LibWeb/HTML/Scripting/ImportMapParseResult.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/TrustedTypes/RequireTrustedTypesForDirective.h>
#include <LibWeb/TrustedTypes/TrustedTypePolicy.h>

namespace Web::HTML {

GC_DEFINE_ALLOCATOR(HTMLScriptElement);

HTMLScriptElement::HTMLScriptElement(DOM::Document& document, DOM::QualifiedName qualified_name)
    : HTMLElement(document, move(qualified_name))
{
}

HTMLScriptElement::~HTMLScriptElement() = default;

void HTMLScriptElement::initialize(JS::Realm& realm)
{
    WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLScriptElement);
    Base::initialize(realm);
}

void HTMLScriptElement::visit_edges(Cell::Visitor& visitor)
{
    Base::visit_edges(visitor);
    if (auto* script = m_result.get_pointer<GC::Ref<Script>>())
        visitor.visit(*script);
    visitor.visit(m_parser_document);
    visitor.visit(m_preparation_time_document);
}

void HTMLScriptElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
    Base::attribute_changed(name, old_value, value, namespace_);

    if (name == HTML::AttributeNames::crossorigin) {
        m_crossorigin = cors_setting_attribute_from_keyword(value);
    } else if (name == HTML::AttributeNames::referrerpolicy) {
        m_referrer_policy = ReferrerPolicy::from_string(value.value_or(""_string)).value_or(ReferrerPolicy::ReferrerPolicy::EmptyString);
    } else if (name == HTML::AttributeNames::src) {
        // https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model:html-element-post-connection-steps-6
        // 1. If namespace is not null, then return.
        if (namespace_.has_value())
            return;

        // AD-HOC: This ensures that prepare_script() is not called when the src attribute is removed.
        //         See: https://github.com/whatwg/html/pull/10188/files#r1685905457 for more information.
        if (!value.has_value())
            return;

        // 2. If localName is src, then run the script HTML element post-connection steps, given element.
        post_connection();
    } else if (name == HTML::AttributeNames::async) {
        // https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model:script-force-async
        // When an async attribute is added to a script element el, the user agent must set el's force async to false.
        m_force_async = false;
    }
}

void HTMLScriptElement::begin_delaying_document_load_event(DOM::Document& document)
{
    // https://html.spec.whatwg.org/multipage/scripting.html#concept-script-script
    // The user agent must delay the load event of the element's node document until the script is ready.
    m_document_load_event_delayer.emplace(document);
}

// https://html.spec.whatwg.org/multipage/scripting.html#execute-the-script-block
void HTMLScriptElement::execute_script()
{
    // https://html.spec.whatwg.org/multipage/document-lifecycle.html#read-html
    // Before any script execution occurs, the user agent must wait for scripts may run for the newly-created document to be true for document.
    if (!m_document->ready_to_run_scripts())
        main_thread_event_loop().spin_until(GC::create_function(heap(), [&] { return m_document->ready_to_run_scripts(); }));

    // 1. Let document be el's node document.
    GC::Ref<DOM::Document> document = this->document();

    // 2. If el's preparation-time document is not equal to document, then return.
    if (m_preparation_time_document.ptr() != document.ptr()) {
        dbgln("HTMLScriptElement: Refusing to run script because the preparation time document is not the same as the node document.");
        return;
    }

    // 3. Unblock rendering on el.
    unblock_rendering();

    // 3. If el's result is null, then fire an event named error at el, and return.
    if (m_result.has<ResultState::Null>()) {
        dbgln("HTMLScriptElement: Refusing to run script because the element's result is null.");
        dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error));
        return;
    }

    // 5. If el's from an external file is true, or el's type is "module", then increment document's ignore-destructive-writes counter.
    bool incremented_destructive_writes_counter = false;
    if (m_from_an_external_file || m_script_type == ScriptType::Module) {
        document->increment_ignore_destructive_writes_counter();
        incremented_destructive_writes_counter = true;
    }

    // 5. Switch on el's type:
    // -> "classic"
    if (m_script_type == ScriptType::Classic) {
        // 1. Let oldCurrentScript be the value to which document's currentScript object was most recently set.
        auto old_current_script = document->current_script();
        // 2. If el's root is not a shadow root, then set document's currentScript attribute to el. Otherwise, set it to null.
        if (!is<DOM::ShadowRoot>(root()))
            document->set_current_script({}, this);
        else
            document->set_current_script({}, nullptr);

        if (m_from_an_external_file)
            dbgln_if(HTML_SCRIPT_DEBUG, "HTMLScriptElement: Running script {}", attribute(HTML::AttributeNames::src).value_or(String {}));
        else
            dbgln_if(HTML_SCRIPT_DEBUG, "HTMLScriptElement: Running inline script");

        // 3. Run the classic script given by el's result.
        (void)as<ClassicScript>(*m_result.get<GC::Ref<Script>>()).run();

        // 4. Set document's currentScript attribute to oldCurrentScript.
        document->set_current_script({}, old_current_script);
    }
    // -> "module"
    else if (m_script_type == ScriptType::Module) {
        // 1. Assert: document's currentScript attribute is null.
        VERIFY(document->current_script() == nullptr);

        // 2. Run the module script given by el's result.
        (void)as<JavaScriptModuleScript>(*m_result.get<GC::Ref<Script>>()).run();
    }
    // -> "importmap"
    else if (m_script_type == ScriptType::ImportMap) {
        // 1. Register an import map given el's relevant global object and el's result.
        m_result.get<GC::Ref<ImportMapParseResult>>()->register_import_map(as<Window>(relevant_global_object(*this)));
    }

    // 7. Decrement the ignore-destructive-writes counter of document, if it was incremented in the earlier step.
    if (incremented_destructive_writes_counter)
        document->decrement_ignore_destructive_writes_counter();

    // 8. If el's from an external file is true, then fire an event named load at el.
    if (m_from_an_external_file)
        dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load));
}

// https://html.spec.whatwg.org/multipage/scripting.html#prepare-a-script
// https://whatpr.org/html/9893/scripting.html#prepare-a-script
void HTMLScriptElement::prepare_script()
{
    // 1. If el's already started is true, then return.
    if (m_already_started) {
        dbgln("HTMLScriptElement: Refusing to run script because it has already started.");
        return;
    }

    // 2. Let parser document be el's parser document.
    GC::Ptr<DOM::Document> parser_document = m_parser_document;

    // 3. Set el's parser document to null.
    m_parser_document = nullptr;

    // 4. If parser document is non-null and el does not have an async attribute, then set el's force async to true.
    if (parser_document && !has_attribute(HTML::AttributeNames::async)) {
        m_force_async = true;
    }

    // 5. Let source text be el's child text content.
    auto source_text = child_text_content();
    auto source_text_utf8 = source_text.to_utf8_but_should_be_ported_to_utf16();

    // 6. If el has no src attribute, and source text is the empty string, then return.
    if (!has_attribute(HTML::AttributeNames::src) && source_text.is_empty()) {
        return;
    }

    // 7. If el is not connected, then return.
    if (!is_connected()) {
        dbgln("HTMLScriptElement: Refusing to run script because the element is not connected.");
        return;
    }

    // 8. If any of the following are true:
    //    - el has a type attribute whose value is the empty string;
    //    - el has no type attribute but it has a language attribute and that attribute's value is the empty string; or
    //    - el has neither a type attribute nor a language attribute
    String script_block_type;
    auto maybe_type_attribute = attribute(HTML::AttributeNames::type);
    auto maybe_language_attribute = attribute(HTML::AttributeNames::language);
    if ((maybe_type_attribute.has_value() && maybe_type_attribute->is_empty())
        || (!maybe_type_attribute.has_value() && maybe_language_attribute.has_value() && maybe_language_attribute->is_empty())
        || (!maybe_type_attribute.has_value() && !maybe_language_attribute.has_value())) {
        // then let the script block's type string for this script element be "text/javascript".
        script_block_type = "text/javascript"_string;
    }
    // Otherwise, if el has a type attribute,
    else if (maybe_type_attribute.has_value()) {
        // then let the script block's type string be the value of that attribute with leading and trailing ASCII whitespace stripped.
        script_block_type = MUST(maybe_type_attribute->trim(Infra::ASCII_WHITESPACE));
    }
    // Otherwise, el has a non-empty language attribute;
    else if (maybe_language_attribute.has_value() && !maybe_language_attribute->is_empty()) {
        // let the script block's type string be the concatenation of "text/" and the value of el's language attribute.
        script_block_type = MUST(String::formatted("text/{}", maybe_language_attribute.value()));
    }

    // 9. If the script block's type string is a JavaScript MIME type essence match,
    if (MimeSniff::is_javascript_mime_type_essence_match(script_block_type)) {
        // then set el's type to "classic".
        m_script_type = ScriptType::Classic;
    }
    // 10. Otherwise, if the script block's type string is an ASCII case-insensitive match for the string "module",
    else if (script_block_type.equals_ignoring_ascii_case("module"sv)) {
        // then set el's type to "module".
        m_script_type = ScriptType::Module;
    }
    // 11. Otherwise, if the script block's type string is an ASCII case-insensitive match for the string "importmap",
    else if (script_block_type.equals_ignoring_ascii_case("importmap"sv)) {
        // then set el's type to "importmap".
        m_script_type = ScriptType::ImportMap;
    }
    // FIXME: 12. Otherwise, if the script block's type string is an ASCII case-insensitive match for the string "speculationrules", then set el's type to "speculationrules".
    // 13. Otherwise, return. (No script is executed, and el's type is left as null.)
    else {
        VERIFY(m_script_type == ScriptType::Null);
        return;
    }

    // 14. If parser document is non-null, then set el's parser document back to parser document and set el's force async to false.
    if (parser_document) {
        m_parser_document = parser_document;
        m_force_async = false;
    }

    // 15. Set el's already started to true.
    m_already_started = true;

    // 16. Set el's preparation-time document to its node document.
    m_preparation_time_document = &document();

    // 17. If parser document is non-null, and parser document is not equal to el's preparation-time document, then return.
    if (parser_document != nullptr && parser_document != m_preparation_time_document) {
        dbgln("HTMLScriptElement: Refusing to run script because the parser document is not the same as the preparation time document.");
        return;
    }

    // 18. If scripting is disabled for el, then return.
    if (is_scripting_disabled()) {
        dbgln("HTMLScriptElement: Refusing to run script because scripting is disabled.");
        return;
    }

    // 19. If el has a nomodule content attribute and its type is "classic", then return.
    if (m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::nomodule)) {
        dbgln("HTMLScriptElement: Refusing to run classic script because it has the nomodule attribute.");
        return;
    }

    // FIXME: 20. Let cspType be "script speculationrules" if el's type is "speculationrules"; otherwise, "script".

    // 21. If el does not have a src content attribute, and the Should element's inline behavior be blocked by Content
    //     Security Policy? algorithm returns "Blocked" when given el, cspType, and source text, then return [CSP]
    if (!has_attribute(AttributeNames::src)
        && ContentSecurityPolicy::should_elements_inline_type_behavior_be_blocked_by_content_security_policy(realm(), *this, ContentSecurityPolicy::Directives::Directive::InlineType::Script, source_text_utf8) == ContentSecurityPolicy::Directives::Directive::Result::Blocked) {
        dbgln("HTMLScriptElement: Refusing to run inline script because it violates the Content Security Policy.");
        return;
    }

    // 22. If el has an event attribute and a for attribute, and el's type is "classic", then:
    if (m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::event) && has_attribute(HTML::AttributeNames::for_)) {
        // 1. Let for be the value of el's' for attribute.
        auto for_ = get_attribute_value(HTML::AttributeNames::for_);

        // 2. Let event be the value of el's event attribute.
        auto event = get_attribute_value(HTML::AttributeNames::event);

        // 3. Strip leading and trailing ASCII whitespace from event and for.
        for_ = MUST(for_.trim(Infra::ASCII_WHITESPACE));
        event = MUST(event.trim(Infra::ASCII_WHITESPACE));

        // 4. If for is not an ASCII case-insensitive match for the string "window", then return.
        if (!for_.equals_ignoring_ascii_case("window"sv)) {
            dbgln("HTMLScriptElement: Refusing to run classic script because the provided 'for' attribute is not equal to 'window'");
            return;
        }

        // 5. If event is not an ASCII case-insensitive match for either the string "onload" or the string "onload()", then return.
        if (!event.equals_ignoring_ascii_case("onload"sv)
            && !event.equals_ignoring_ascii_case("onload()"sv)) {
            dbgln("HTMLScriptElement: Refusing to run classic script because the provided 'event' attribute is not equal to 'onload' or 'onload()'");
            return;
        }
    }

    // 23. If el has a charset attribute, then let encoding be the result of getting an encoding from the value of the charset attribute.
    //     If el does not have a charset attribute, or if getting an encoding failed, then let encoding be el's node document's the encoding.
    Optional<String> encoding;

    if (has_attribute(HTML::AttributeNames::charset)) {
        auto charset = TextCodec::get_standardized_encoding(get_attribute_value(HTML::AttributeNames::charset));
        if (charset.has_value())
            encoding = String::from_utf8(*charset).release_value_but_fixme_should_propagate_errors();
    }

    if (!encoding.has_value()) {
        encoding = document().encoding_or_default();
    }

    VERIFY(encoding.has_value());

    // 24. Let classic script CORS setting be the current state of el's crossorigin content attribute.
    auto classic_script_cors_setting = m_crossorigin;

    // 25. Let module script credentials mode be the CORS settings attribute credentials mode for el's crossorigin content attribute.
    auto module_script_credential_mode = cors_settings_attribute_credentials_mode(m_crossorigin);

    // 26. Let cryptographic nonce be el's [[CryptographicNonce]] internal slot's value.
    auto cryptographic_nonce = m_cryptographic_nonce;

    // 27. If el has an integrity attribute, then let integrity metadata be that attribute's value.
    //     Otherwise, let integrity metadata be the empty string.
    String integrity_metadata;
    if (auto maybe_integrity = attribute(HTML::AttributeNames::integrity); maybe_integrity.has_value()) {
        integrity_metadata = *maybe_integrity;
    }

    // 28. Let referrer policy be the current state of el's referrerpolicy content attribute.
    auto referrer_policy = m_referrer_policy;

    // 29. Let fetch priority be the current state of el's fetchpriority content attribute.
    auto fetch_priority = Fetch::Infrastructure::request_priority_from_string(get_attribute_value(HTML::AttributeNames::fetchpriority)).value_or(Fetch::Infrastructure::Request::Priority::Auto);

    // 30. Let parser metadata be "parser-inserted" if el is parser-inserted, and "not-parser-inserted" otherwise.
    auto parser_metadata = is_parser_inserted()
        ? Fetch::Infrastructure::Request::ParserMetadata::ParserInserted
        : Fetch::Infrastructure::Request::ParserMetadata::NotParserInserted;

    // 31. Let options be a script fetch options whose cryptographic nonce is cryptographic nonce,
    //     integrity metadata is integrity metadata, parser metadata is parser metadata,
    //     credentials mode is module script credentials mode, referrer policy is referrer policy,
    //     and fetch priority is fetch priority.
    ScriptFetchOptions options {
        .cryptographic_nonce = move(cryptographic_nonce),
        .integrity_metadata = move(integrity_metadata),
        .parser_metadata = parser_metadata,
        .credentials_mode = module_script_credential_mode,
        .referrer_policy = move(referrer_policy),
        .fetch_priority = move(fetch_priority),
    };

    // 32. Let settings object be el's node document's relevant settings object.
    auto& settings_object = document().relevant_settings_object();

    // 33. If el has a src content attribute, then:
    if (has_attribute(HTML::AttributeNames::src)) {
        // 1. If el's type is "importmap" or "speculationrules", then:
        // FIXME: Add "speculationrules" support.
        if (m_script_type == ScriptType::ImportMap) {
            // then queue an element task on the DOM manipulation task source given el to fire an event named error at el, and return.
            queue_an_element_task(HTML::Task::Source::DOMManipulation, [this] {
                dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error));
            });
            return;
        }

        // 2. Let src be the value of el's src attribute.
        auto src = get_attribute_value(HTML::AttributeNames::src);

        // 3. If src is the empty string, then queue an element task on the DOM manipulation task source given el to fire an event named error at el, and return.
        if (src.is_empty()) {
            dbgln("HTMLScriptElement: Refusing to run script because the src attribute is empty.");
            queue_an_element_task(HTML::Task::Source::DOMManipulation, [this] {
                dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error));
            });
            return;
        }

        // 4. Set el's from an external file to true.
        m_from_an_external_file = true;

        // 5. Let url be the result of encoding-parsing a URL given src, relative to el's node document.
        auto url = document().encoding_parse_url(src);

        // 6. If url is failure, then queue an element task on the DOM manipulation task source given el to fire an event named error at el, and return.
        if (!url.has_value()) {
            dbgln("HTMLScriptElement: Refusing to run script because the src URL '{}' is invalid.", url);
            queue_an_element_task(HTML::Task::Source::DOMManipulation, [this] {
                dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error));
            });
            return;
        }

        // 7. If el is potentially render-blocking, then block rendering on el.
        if (is_potentially_render_blocking()) {
            block_rendering();
        }

        // 8. Set el's delaying the load event to true.
        begin_delaying_document_load_event(*m_preparation_time_document);

        // FIXME: 9. If el is currently render-blocking, then set options's render-blocking to true.

        // 10. Let onComplete given result be the following steps:
        OnFetchScriptComplete on_complete = create_on_fetch_script_complete(heap(), [this](auto result) {
            // 1. Mark as ready el given result.
            if (result)
                mark_as_ready(Result { *result });
            else
                mark_as_ready(ResultState::Null {});
        });

        // 11. Switch on el's type:
        // -> "classic"
        if (m_script_type == ScriptType::Classic) {
            // Fetch a classic script given url, settings object, options, classic script CORS setting, encoding, and onComplete.
            fetch_classic_script(*this, *url, settings_object, move(options), classic_script_cors_setting, encoding.release_value(), on_complete).release_value_but_fixme_should_propagate_errors();
        }
        // -> "module"
        else if (m_script_type == ScriptType::Module) {
            // If el does not have an integrity attribute, then set options's integrity metadata to the result of resolving a module integrity metadata with url and settings object.
            if (!has_attribute(HTML::AttributeNames::integrity))
                options.integrity_metadata = resolve_a_module_integrity_metadata(*url, settings_object);

            // Fetch an external module script graph given url, settings object, options, and onComplete.
            fetch_external_module_script_graph(realm(), *url, settings_object, options, on_complete);
        }
    }

    // 34. If el does not have a src content attribute:
    if (!has_attribute(HTML::AttributeNames::src)) {
        // 1. Let base URL be el's node document's document base URL.
        auto base_url = document().base_url();

        // 2. Switch on el's type:
        // -> "classic"
        if (m_script_type == ScriptType::Classic) {
            // 1. Let script be the result of creating a classic script using source text, settings object's realm, base URL, and options.
            // FIXME: Pass options.
            auto script = ClassicScript::create(m_document->url().to_byte_string(), source_text_utf8, settings_object.realm(), base_url, m_source_line_number);

            // 2. Mark as ready el given script.
            mark_as_ready(Result(move(script)));
        }
        // -> "module"
        else if (m_script_type == ScriptType::Module) {
            // 1. Set el's delaying the load event to true.
            begin_delaying_document_load_event(*m_preparation_time_document);

            auto steps = create_on_fetch_script_complete(heap(), [this](auto result) {
                // 1. Queue an element task on the networking task source given el to perform the following steps:
                queue_an_element_task(Task::Source::Networking, [this, result = move(result)] {
                    // 1. Mark as ready el given result.
                    if (!result)
                        mark_as_ready(ResultState::Null {});
                    else
                        mark_as_ready(Result(*result));
                });
            });

            // 2. Fetch an inline module script graph, given source text, base URL, settings object, options, and with the following steps given result:
            // FIXME: Pass options
            fetch_inline_module_script_graph(realm(), m_document->url().to_byte_string(), source_text.to_byte_string(), base_url, document().relevant_settings_object(), steps);
        }
        // -> "importmap"
        else if (m_script_type == ScriptType::ImportMap) {
            // 1. Let result be the result of creating an import map parse result given source text and base URL.
            auto result = ImportMapParseResult::create(realm(), source_text.to_byte_string(), base_url);

            // 2. Mark as ready el given result.
            mark_as_ready(Result(move(result)));
        }
        // FIXME: -> "speculationrules"
    }

    // 35. If el's type is "classic" and el has a src attribute, or el's type is "module":
    if ((m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::src)) || m_script_type == ScriptType::Module) {
        // 1. Assert: el's result is "uninitialized".
        // FIXME: I believe this step to be a spec bug, and it should be removed: https://github.com/whatwg/html/issues/8534

        // 2. If el has an async attribute or el's force async is true:
        if (has_attribute(HTML::AttributeNames::async) || m_force_async) {
            // 1. Let scripts be el's preparation-time document's set of scripts that will execute as soon as possible.
            // 2. Append el to scripts.
            m_preparation_time_document->scripts_to_execute_as_soon_as_possible().append(*this);

            // 3. Set el's steps to run when the result is ready to the following:
            m_steps_to_run_when_the_result_is_ready = [this] {
                // 1. Execute the script element el.
                execute_script();

                // 2. Remove el from scripts.
                m_preparation_time_document->scripts_to_execute_as_soon_as_possible().remove_first_matching([this](auto& entry) {
                    return entry.ptr() == this;
                });
            };
        }

        // 3. Otherwise, if el is not parser-inserted:
        else if (!is_parser_inserted()) {
            // 1. Let scripts be el's preparation-time document's list of scripts that will execute in order as soon as possible.
            // 2. Append el to scripts.
            m_preparation_time_document->scripts_to_execute_in_order_as_soon_as_possible().append(*this);

            // 3. Set el's steps to run when the result is ready to the following:
            m_steps_to_run_when_the_result_is_ready = [this] {
                auto& scripts = m_preparation_time_document->scripts_to_execute_in_order_as_soon_as_possible();
                // 1. If scripts[0] is not el, then abort these steps.
                if (scripts[0] != this)
                    return;

                // 2. While scripts is not empty, and scripts[0]'s result is not "uninitialized":
                while (!scripts.is_empty() && !scripts[0]->m_result.has<ResultState::Uninitialized>()) {
                    // 1. Execute the script element scripts[0].
                    scripts[0]->execute_script();

                    // 2. Remove scripts[0].
                    scripts.take_first();
                }
            };
        }

        // 4. Otherwise, if el has a defer attribute or el's type is "module":
        else if (has_attribute(HTML::AttributeNames::defer) || m_script_type == ScriptType::Module) {
            // 1. Append el to its parser document's list of scripts that will execute when the document has finished parsing.
            m_parser_document->add_script_to_execute_when_parsing_has_finished({}, *this);

            // 2. Set el's steps to run when the result is ready to the following:
            m_steps_to_run_when_the_result_is_ready = [this] {
                // set el's ready to be parser-executed to true. (The parser will handle executing the script.)
                m_ready_to_be_parser_executed = true;
            };
        }

        // 5. Otherwise:
        else {
            // 1. Set el's parser document's pending parsing-blocking script to el.
            m_parser_document->set_pending_parsing_blocking_script(this);

            // FIXME: 2. Block rendering on el.

            // 3. Set el's steps to run when the result is ready to the following:
            m_steps_to_run_when_the_result_is_ready = [this] {
                // set el's ready to be parser-executed to true. (The parser will handle executing the script.)
                m_ready_to_be_parser_executed = true;
            };
        }
    }

    // 36. Otherwise:
    else {
        // 1. Assert: el's result is not "uninitialized".
        VERIFY(!m_result.has<ResultState::Uninitialized>());

        // 2. If all of the following are true:
        //    - el's type is "classic";
        //    - el is parser-inserted;
        //    - el's parser document has a style sheet that is blocking scripts; and
        //    FIXME: - either the parser that created el is an XML parser, or it's an HTML parser whose script nesting level is not greater than one,
        //    then:
        if (m_script_type == ScriptType::Classic
            && is_parser_inserted()
            && m_parser_document->has_a_style_sheet_that_is_blocking_scripts()) {
            // 1. Set el's parser document's pending parsing-blocking script to el.
            m_parser_document->set_pending_parsing_blocking_script(this);

            // 2. Set el's ready to be parser-executed to true. (The parser will handle executing the script.)
            m_ready_to_be_parser_executed = true;
        }

        // 3. Otherwise,
        else {
            // immediately execute the script element el, even if other scripts are already executing.
            execute_script();
        }
    }
}

// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model:html-element-post-connection-steps-4
void HTMLScriptElement::children_changed(ChildrenChangedMetadata const* metadata)
{
    Base::children_changed(metadata);

    // 1. Run the script HTML element post-connection steps, given the script element.
    post_connection();
}

// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model:prepare-the-script-element-5
void HTMLScriptElement::post_connection()
{
    // 1. If insertedNode is not connected, then return.
    if (!is_connected())
        return;

    // 2. If insertedNode is parser-inserted, then return.
    if (is_parser_inserted())
        return;

    // 3. Prepare the script element given insertedNode.
    prepare_script();
}

// https://html.spec.whatwg.org/multipage/scripting.html#mark-as-ready
void HTMLScriptElement::mark_as_ready(Result result)
{
    // 1. Set el's result to result.
    m_result = move(result);

    // 2. If el's steps to run when the result is ready are not null, then run them.
    if (m_steps_to_run_when_the_result_is_ready)
        m_steps_to_run_when_the_result_is_ready();

    // 3. Set el's steps to run when the result is ready to null.
    m_steps_to_run_when_the_result_is_ready = nullptr;

    // 4. Set el's delaying the load event to false.
    m_document_load_event_delayer.clear();
}

void HTMLScriptElement::unmark_as_already_started(Badge<DOM::Range>)
{
    m_already_started = false;
}

void HTMLScriptElement::unmark_as_parser_inserted(Badge<DOM::Range>)
{
    m_parser_document = nullptr;
}

// https://github.com/w3c/trusted-types/blob/0f01aee35c3684e8cb20e7d83445ad65938f545d/spec/index.bs#L1341
TrustedTypes::TrustedScriptURLOrString HTMLScriptElement::src() const
{
    // 1. Let element be the result of running this's get the element.

    // 2. Let contentAttributeValue be the result of running this's get the content attribute.
    auto maybe_src = get_attribute(AttributeNames::src);

    // 3. If contentAttributeValue is null, then return the empty string.
    if (!maybe_src.has_value())
        return Utf16String {};

    auto const& raw = *maybe_src;

    // 4. Let urlString be the result of encoding-parsing-and-serializing a URL given contentAttributeValue, relative to element's node document.
    auto url_string = document().encoding_parse_and_serialize_url(raw);

    // 5. If urlString is not failure, then return urlString.
    if (url_string.has_value())
        return Utf16String::from_utf8_without_validation(*url_string);

    // 6. Return contentAttributeValue, converted to a scalar value string.
    return Utf16String {};
}

// https://www.w3.org/TR/trusted-types/#the-text-idl-attribute
WebIDL::ExceptionOr<void> HTMLScriptElement::set_text(TrustedTypes::TrustedScriptOrString text)
{
    // 1. Let value be the result of calling Get Trusted Type compliant string with
    //    TrustedScript, this’s relevant global object, the given value, HTMLScriptElement text, and script.
    auto const value = TRY(get_trusted_type_compliant_string(
        TrustedTypes::TrustedTypeName::TrustedScript,
        HTML::relevant_global_object(*this),
        text,
        TrustedTypes::InjectionSink::HTMLScriptElementtext,
        TrustedTypes::Script.to_string()));

    // 2. Set this’s script text value to the given value.
    m_script_text = value;

    // 3. String replace all with the given value within this.
    string_replace_all(value);
    return {};
}

// https://www.w3.org/TR/trusted-types/#the-src-idl-attribute
WebIDL::ExceptionOr<void> HTMLScriptElement::set_src(TrustedTypes::TrustedScriptURLOrString text)
{
    // 1. Let value be the result of calling Get Trusted Type compliant string with
    //    TrustedScriptURL, this’s relevant global object, the given value, HTMLScriptElement src, and script.
    auto const value = TRY(TrustedTypes::get_trusted_type_compliant_string(
        TrustedTypes::TrustedTypeName::TrustedScriptURL,
        HTML::relevant_global_object(*this),
        text,
        TrustedTypes::InjectionSink::HTMLScriptElementsrc,
        TrustedTypes::Script.to_string()));

    // 2. Set this’s src content attribute to value.
    TRY(set_attribute(AttributeNames::src, value));
    return {};
}

// https://w3c.github.io/trusted-types/dist/spec/#the-textContent-idl-attribute
Variant<GC::Root<TrustedTypes::TrustedScript>, Utf16String, Empty> HTMLScriptElement::text_content() const
{
    // 1. Return the result of running get text content with this.
    return descendant_text_content();
}

// https://w3c.github.io/trusted-types/dist/spec/#the-textContent-idl-attribute
WebIDL::ExceptionOr<void> HTMLScriptElement::set_text_content(TrustedTypes::TrustedScriptOrString text)
{
    // 1. Let value be the result of calling Get Trusted Type compliant string with
    //    TrustedScript, this’s relevant global object, the given value, HTMLScriptElement textContent, and script.
    auto const value = TRY(TrustedTypes::get_trusted_type_compliant_string(
        TrustedTypes::TrustedTypeName::TrustedScript,
        HTML::relevant_global_object(*this),
        text,
        TrustedTypes::InjectionSink::HTMLScriptElementtextContent,
        TrustedTypes::Script.to_string()));

    // 2. Set this’s script text value to value.
    m_script_text = value;

    // 3. Run set text content with this and value.
    string_replace_all(value);
    return {};
}

// https://w3c.github.io/trusted-types/dist/spec/#the-innerText-idl-attribute
TrustedTypes::TrustedScriptOrString HTMLScriptElement::inner_text()
{
    // 1. Return the result of running get text content with this.
    return get_the_text_steps();
}

// https://w3c.github.io/trusted-types/dist/spec/#the-innerText-idl-attribute
WebIDL::ExceptionOr<void> HTMLScriptElement::set_inner_text(TrustedTypes::TrustedScriptOrString text)
{
    // 1. Let value be the result of calling Get Trusted Type compliant string with
    //    TrustedScript, this’s relevant global object, the given value, HTMLScriptElement innerText, and script.
    auto const value = TRY(TrustedTypes::get_trusted_type_compliant_string(
        TrustedTypes::TrustedTypeName::TrustedScript,
        HTML::relevant_global_object(*this),
        text,
        TrustedTypes::InjectionSink::HTMLScriptElementinnerText,
        TrustedTypes::Script.to_string()));

    // 2. Set this’s script text value to value.
    m_script_text = value;

    // 3. Run set the inner text steps with this and value.
    HTMLElement::set_inner_text(value);
    return {};
}

// https://html.spec.whatwg.org/multipage/scripting.html#dom-script-async
bool HTMLScriptElement::async() const
{
    // 1. If this's force async is true, then return true.
    if (m_force_async)
        return true;

    // 2. If this's async content attribute is present, then return true.
    if (has_attribute(HTML::AttributeNames::async))
        return true;

    // 3. Return false.
    return false;
}

void HTMLScriptElement::set_async(bool async)
{
    // 1. Set this's force async to false.
    m_force_async = false;

    // 2. If the given value is true, then set this's async content attribute to the empty string.
    if (async) {
        MUST(set_attribute(HTML::AttributeNames::async, ""_string));
    }
    // 3. Otherwise, remove this's async content attribute.
    else {
        remove_attribute(HTML::AttributeNames::async);
    }
}

// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model:concept-node-clone-ext
WebIDL::ExceptionOr<void> HTMLScriptElement::cloned(Node& copy, bool subtree) const
{
    TRY(Base::cloned(copy, subtree));

    // The cloning steps for script elements given node, copy, and subtree are to set copy's already started to node's already started.
    auto& script_copy = as<HTMLScriptElement>(copy);
    script_copy.m_already_started = m_already_started;

    return {};
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#implicitly-potentially-render-blocking
bool HTMLScriptElement::is_implicitly_potentially_render_blocking() const
{
    // A script element el is implicitly potentially render-blocking if el's type is "classic", el is parser-inserted, and el does not have an async or defer attribute.
    return m_script_type == ScriptType::Classic && is_parser_inserted() && !has_attribute(AttributeNames::async) && !has_attribute(AttributeNames::defer);
}

}
